"""
    xfree.cli.main.py
    ~~~~~~~~~~~~~~~~~~~~~
    文件描述：XFree脚本源主程序
    作者：xfree
    邮箱：cy_83k@qq.com
"""
import binascii
import os
import time
import sys
import traceback
import logging
from datetime import datetime

from sqlalchemy_utils.functions import database_exists
import click
from flask import current_app
from flask.cli import FlaskGroup, ScriptInfo, with_appcontext
from flask_alembic.cli import cli as alembic_click
from xfree import create_app
from xfree.cli.utils import (
    EmailType,
    get_version,
    prompt_save_user,
    write_config,
    XFreeCLIError,
    prompt_config_path,
)
from xfree.extensions import db, whooshee, alembic,celery
from xfree.utils.populate import (
    create_default_groups,
    create_default_settings,
    create_latest_db,
    create_test_data,
    create_welcome_forum,
    insert_bulk_data,
    run_plugin_migrations,
    update_settings_from_fixture,
)
from xfree.utils.translations import compile_translations
from werkzeug.utils import import_string
from jinja2 import Environment, FileSystemLoader

logger = logging.getLogger(__name__)

class XFreeGroup(FlaskGroup):
    """
    这个类继承自FlaskGroup，并添加了一些自定义的命令和选项。
    属性：
    - _loaded_xfree_plugins: 是否加载了xfree插件
    方法：
    - __init__(self, *args, **kwargs): 初始化方法，调用父类的初始化方法，并初始化_loaded_xfree_plugins为False
    - _load_xfree_plugins(self, ctx): 加载XFRee插件

    """

    def __init__(self, *args, **kwargs):
        super(XFreeGroup, self).__init__(*args, **kwargs)
        # 属性是否加载了xfree插件
        self._loaded_xfree_plugins = False


    def _load_xfree_plugins(self, ctx):
        """
        加载XFRee插件。
        :param ctx: click上下文
        :return: None
        """
        # 如果已经加载了 xfree 插件，则直接返回
        if self._loaded_xfree_plugins:
            return

        try:
            # 加载插件：获取应用对象
            app = ctx.ensure_object(ScriptInfo).load_app()

            # 触发插件的钩子函数，将当前命令行工具对象和应用对象传递给插件
            app.pluggy.hook.xfree_cli(cli=self, app=app)
            # 设置标志，表示 xfree 插件已加载
            self._loaded_xfree_plugins = True
        except Exception:
            # 如果在加载插件过程中出现错误，记录错误信息
            logger.error(
                "加载 CLI 插件时出错", exc_info=traceback.format_exc()
            )
        else:
            # 获取 shell 上下文处理器列表
            shell_context_processors = app.pluggy.hook.xfree_shell_context()
            # 遍历每个 shell 上下文处理器，并将其添加到应用的 shell 上下文处理器列表中
            for p in shell_context_processors:
                app.shell_context_processor(p)

    def get_command(self, ctx, name):
        """获取命令。"""
        # 加载 xfree 插件
        self._load_xfree_plugins(ctx)
        # 调用父类的 get_command 方法来获取指定名称的命令
        return super(XFreeGroup, self).get_command(ctx, name)

    def list_commands(self, ctx):
        """列出命令。"""
        self._load_xfree_plugins(ctx)
        return super(XFreeGroup, self).list_commands(ctx)


def make_app():
    """创建XFree应用程序。"""
    ctx = click.get_current_context(silent=True)
    script_info = None
    if ctx is not None:
        script_info = ctx.obj

    config_file = getattr(script_info, "config_file", None)
    instance_path = getattr(script_info, "instance_path", None)
    return create_app(config_file, instance_path)


def set_config(ctx, param, value):
    """设置配置文件。"""
    ctx.ensure_object(ScriptInfo).config_file = value


def set_instance(ctx, param, value):
    """设置实例路径。"""
    ctx.ensure_object(ScriptInfo).instance_path = value


@click.group(
    cls=XFreeGroup,
    create_app=make_app,
    add_version_option=False,
    invoke_without_command=True,
)
@click.option(
    "--config",
    expose_value=False,
    callback=set_config,
    required=False,
    is_flag=False,
    is_eager=True,
    metavar="配置",
    help="指定要在虚线模块中使用的配置符号，例如 'xfree.config.default.DefaultConfig'或使用像 '/path/to/xfree.cfg' 这样的路径",
)
@click.option(
    "--instance",
    expose_value=False,
    callback=set_instance,
    required=False,
    is_flag=False,
    is_eager=True,
    metavar="路径",
    help="指定要使用的实例路径。默认情况下，文件夹包或模块旁边的 ' instance ' 被假定为成为实例路径。 ",
)
@click.option(
    "--version",
    "-V",
    expose_value=False,
    callback=get_version,
    is_flag=True,
    is_eager=True,
    metavar="版本",
    help="显示XFree版本。",
)
@click.pass_context
def xfree(ctx):
    """xfree 命令行"""
    if ctx.invoked_subcommand is None:
        # 显示帮助文本而不是错误
        click.echo(ctx.get_help())

# flask_alembic 一个与 Flask 应用和数据库迁移相关的库,通过导入这个对象并赋予一个新的名称，代码的其他部分可以使用 alembic_click 来调用与数据库迁移相关的功能。
xfree.add_command(alembic_click, "db")


@xfree.command()
@click.option(
    "--welcome",
    "-w",
    default=True,
    is_flag=True,
    help="禁用欢迎论坛。"
)
@click.option(
    "--force",
    "-f",
    default=False,
    is_flag=True,
    help="不请求确认。"
)
@click.option(
    "--username",
    "-u",
    help="用户的用户名。"
)
@click.option(
    "--email",
    "-e",
    type=EmailType(),
    help="用户的电子邮件地址。"
)
@click.option(
    "--password",
    "-p",
    help="用户的密码。"
)
@click.option(
    "--no-plugins",
    "-n",
    default=False,
    is_flag=True,
    help="不运行默认插件的迁移。"
)
@with_appcontext
def install(welcome, force, username, email, password, no_plugins):
    """安装 xfree。如果不使用参数，则交互式设置将被运行
    :param welcome: 是否创建欢迎论坛
    :param force: 是否强制删除现有数据库
    :param username: 用户名
    :param email: 电子邮件
    :param password: 密码
    :param no_plugins: 是否运行默认插件
    :return:
    """
    # 检查配置文件是否存在
    if not current_app.config["CONFIG_PATH"]:
        click.secho(
            "[!] 未找到'xfree.cfg'个人配置文件。您可以使用'xfree makeconfig'命令来生成配置文件。",
            fg="red",
        )
        import sys
        sys.exit(1)

    click.secho("[+] 正在安装 XFree...", fg="cyan")

    # 检查数据库是否存在
    if database_exists(db.engine.url):
        if force or click.confirm(
                click.style(
                    "已找到现有数据库。您是否要删除旧数据库并创建新数据库？",
                    fg="magenta",
                )
        ):
            db.drop_all()
        else:
            import sys
            sys.exit(0)

    # 从头创建数据库并进行“标记”
    create_latest_db()

    click.secho("[+] 正在创建默认设置...", fg="cyan")
    # 创建默认组
    create_default_groups()
    # 创建默认设置
    create_default_settings()

    click.secho("[+] 正在创建管理员用户...", fg="cyan")
    # 创建管理员用户
    prompt_save_user(username, email, password, "admin")

    if welcome:
        click.secho("[+] 正在创建欢迎论坛...", fg="cyan")
        create_welcome_forum()

    if not no_plugins:
        click.secho("[+] 正在安装默认插件...", fg="cyan")
        run_plugin_migrations()

    click.secho("[+] 正在编译翻译文件...", fg="cyan")
    compile_translations()

    click.secho("[+] XFree 已成功安装！", fg="green", bold=True)


@xfree.command()
@click.option(
    "--test-data", "-t", default=False, is_flag=True, help="添加一些测试数据。"
)
@click.option(
    "--bulk-data", "-b", default=False, is_flag=True, help="添加大量数据。"
)
@click.option(
    "--posts",
    default=100,
    help="每个主题中创建的帖子数量（默认：100）。",
)
@click.option(
    "--topics", default=100, help="创建的主题数量（默认：100）。"
)
@click.option(
    "--force", "-f", is_flag=True, help="在填充数据之前将删除数据库。"
)
@click.option(
    "--initdb",
    "-i",
    is_flag=True,
    help="在填充数据之前初始化数据库。",
)
def populate(bulk_data, test_data, posts, topics, force, initdb):
    """创建 XFree 所需的表和组。
    ：param bulk_data: 是否添加大量数据
    :param test_data: 是否添加一些测试数据
    :param posts: 每个主题中创建的帖子数量
    :param topics: 创建的主题数量
    :param force: 是否强制删除现有数据库
    :param initdb: 是否初始化数据库

    """
    if force:
        # 如果指定了 --force 选项，则显示重新创建数据库的消息
        click.secho("[+] 正在重新创建数据库...", fg="cyan")
        # 删除所有表
        db.drop_all()

        # 如果没有传递 -i 选项，则不初始化数据库
        if not initdb:
            create_latest_db()

    if initdb:
        # 如果指定了 --initdb 选项，则显示初始化数据库的消息
        click.secho("[+] 正在初始化数据库...", fg="cyan")
        create_latest_db()
        run_plugin_migrations()

    if test_data:
        # 如果指定了 --test-data 选项，则显示添加一些测试数据的消息
        click.secho("[+] 正在添加一些测试数据...", fg="cyan")
        create_test_data()

    if bulk_data:
        # 如果指定了 --bulk-data 选项，则显示添加大量测试数据的消息
        click.secho("[+] 正在添加大量测试数据...", fg="cyan")
        timer = time.time()
        # 插入大量数据，并返回创建的主题和帖子数量
        rv = insert_bulk_data(int(topics), int(posts))
        if not rv and not test_data:
            create_test_data()
            rv = insert_bulk_data(int(topics), int(posts))
        elapsed = time.time() - timer
        # 显示创建数据所花费的时间以及创建的主题和帖子数量
        click.secho(
            "[+] 创建 {} 个主题和 {} 个帖子花费了 {:.2f} 秒。".format(
                rv[0], rv[1], elapsed
            ),
            fg="cyan",
        )

    # 如果既没有指定 --test-data 也没有指定 --bulk-data 选项，则使用默认数据填充数据库
    if not test_data and not bulk_data:
        click.secho("[+] 正在使用一些默认值填充数据库...", fg="cyan")
        create_default_groups()
        create_default_settings()

@xfree.command()
def reindex():
    """重新索引搜索索引。"""
    click.secho("[+] 正在重新索引搜索索引...", fg="cyan")
    whooshee.reindex()


@xfree.command()
@click.option(
    "all_latest",
    "--all",
    "-a",
    default=False,
    is_flag=True,
    help="将迁移和固定装置升级到最新版本。",
)
@click.option(
    "--fixture/",
    "-f",
    default=None,
    help="应该升级或安装的固定装置。",
)
@click.option(
    "--force", default=False, is_flag=True, help="强制升级固定装置。",
)
def upgrade(all_latest, fixture, force):
    """更新迁移和固定装置。"""
    if all_latest:
        click.secho("[+] 正在将迁移升级到最新版本...", fg="cyan")
        alembic.upgrade()

    if fixture or all_latest:
        try:
            # 尝试导入指定的固定装置模块
            settings = import_string("xfree.fixtures.{}".format(fixture))
            settings = settings.fixture
        except ImportError:
            # 如果导入失败，抛出错误
            raise XFreeCLIError(
                "{} 固定装置不可用".format(fixture), fg="red"
            )

        click.secho("[+] 正在更新固定装置...", fg="cyan")
        # 从固定装置更新设置，并返回更新的设置组和数量
        count = update_settings_from_fixture(
            fixture=settings, overwrite_group=force, overwrite_setting=force
        )
        click.secho(
            "[+] {} 个设置组中的 {} 个设置已更新。".format(
                len(count), sum(len(settings) for settings in count.values())
            ),
            fg="green",
        )


@xfree.command(
    "celery",
    add_help_option=False,
    context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
)
@click.pass_context
@with_appcontext
def start_celery(ctx):
    """围绕 'celery' 命令的预配置包装器。"""
    celery.start(ctx.args)


@xfree.command("shell", short_help="在应用上下文中运行一个 shell。")
@with_appcontext
def shell_command():
    """在给定的 Flask 应用上下文中运行一个交互式 Python shell。应用将根据其配置填充这个 shell 的默认命名空间。
    这对于执行小段管理代码而无需手动配置应用非常有用。

    这段代码片段取自 Flask 的 cli 模块，并进行了修改以运行 IPython，如果 IPython 不可用，则回退到普通 shell。
    """
    import code

    banner = "Python %s on %s\n实例路径: %s" % (
        sys.version,
        sys.platform,
        current_app.instance_path,
    )
    ctx = {"db": db}

    # 支持常规 Python 解释器启动脚本，如果有人在使用它。
    startup = os.environ.get("PYTHONSTARTUP")
    if startup and os.path.isfile(startup):
        with open(startup, "r") as f:
            eval(compile(f.read(), startup, "exec"), ctx)

    ctx.update(current_app.make_shell_context())

    try:
        import IPython
        from traitlets.config import get_config

        c = get_config()
        # 这使得提示符再次使用颜色
        c.InteractiveShellEmbed.colors = "Linux"
        IPython.embed(config=c, banner1=banner, user_ns=ctx)
    except ImportError:
        code.interact(banner=banner, local=ctx)


@xfree.command("urls", short_help="显示应用的路由。")
@click.option(
    "--route", "-r", "order_by", flag_value="rule", default=True, help="按路由排序",
)
@click.option(
    "--endpoint", "-e", "order_by", flag_value="endpoint", help="按端点排序",
)
@click.option(
    "--methods", "-m", "order_by", flag_value="methods", help="按方法排序",
)
@with_appcontext
def list_urls(order_by):
    """列出所有可用的路由。"""
    from flask import current_app

    # 根据指定的排序方式对路由进行排序
    rules = sorted(
        current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order_by)
    )

    max_rule_len = max(len(rule.rule) for rule in rules)
    max_rule_len = max(max_rule_len, len("Route"))

    max_endpoint_len = max(len(rule.endpoint) for rule in rules)
    max_endpoint_len = max(max_endpoint_len, len("Endpoint"))

    max_method_len = max(len(", ".join(rule.methods)) for rule in rules)
    max_method_len = max(max_method_len, len("Methods"))

    column_header_len = max_rule_len + max_endpoint_len + max_method_len + 4
    column_template = "{:<%s}  {:<%s}  {:<%s}" % (
        max_rule_len,
        max_endpoint_len,
        max_method_len,
    )

    click.secho(
        column_template.format("Route", "Endpoint", "Methods"), fg="blue", bold=True
    )
    click.secho("=" * column_header_len, bold=True)

    for rule in rules:
        methods = ", ".join(rule.methods)
        click.echo(column_template.format(rule.rule, rule.endpoint, methods))


@xfree.command("makeconfig")
@click.option(
    "--development",
    "-d",
    default=False,
    is_flag=True,
    help="创建一个带有 DEBUG 设置为 True 的开发配置。",
)
@click.option(
    "--output",
    "-o",
    required=False,
    help="配置文件将保存的路径。如果未指定，则默认为 xfree 的根文件夹。",
)
@click.option(
    "--force",
    "-f",
    default=False,
    is_flag=True,
    help="如果存在现有配置文件，则覆盖它。",
)
def generate_config(development, output, force):
    """生成一个 xfree 配置文件。
    :param development: 如果为 True，则生成一个带有 DEBUG 设置为 True 的配置文件。
    如果为 False，则生成一个带有 DEBUG 设置为 False 的配置文件。
    :param output: 配置文件将保存的路径。如果未指定，则默认为 ./xfree/configs/ 的根文件夹。
    :param force: 如果为 True，则覆盖现有配置文件。 如果为 False，则不会覆盖现有配置文件。
   :return:
    """
    # 获取配置模板的路径
    config_env = Environment(
        loader=FileSystemLoader(os.path.join(current_app.root_path, "configs"))
    )
    config_template = config_env.get_template("config.cfg.template")
    # 判断是否传入路径参数
    if output:
        config_path = os.path.abspath(output)
    else:
        config_path = os.path.dirname(os.path.join(current_app.root_path, "configs"))
    # 判断配置文件是否存在
    if os.path.exists(config_path) and not os.path.isfile(config_path):
        config_path = os.path.join(config_path, "xfree.cfg")

    # 处理 Windows 环境下的数据库路径
    database_path = "sqlite:///" + os.path.join(
        os.path.dirname(current_app.instance_path), "xfree.sqlite"
    )
    if os.name == "nt":
        database_path = database_path.replace("\\", r"\\")

    default_conf = {
        "is_debug": False,
        "server_name": "example.org",
        "use_https": True,
        "database_uri": database_path,
        "redis_enabled": False,
        "redis_uri": "redis://localhost:6379",
        "mail_server": "localhost",
        "mail_port": 25,
        "mail_use_tls": False,
        "mail_use_ssl": False,
        "mail_username": "",
        "mail_password": "",
        "mail_sender_name": "xfree Mailer",
        "mail_sender_address": "cy_83k@qq.com",
        "mail_admin_address": "cy_83k@qq.com",
        "secret_key": binascii.hexlify(os.urandom(24)).decode(),
        "csrf_secret_key": binascii.hexlify(os.urandom(24)).decode(),
        "timestamp": datetime.utcnow().strftime("%A, %d. %B %Y at %H:%M"),
        "log_config_path": "",
        "deprecation_level": "default",
    }

    if not force:
        config_path = prompt_config_path(config_path)

    if force and os.path.exists(config_path):
        click.secho(
            "覆盖现有配置文件: {}".format(config_path), fg="yellow"
        )

    if development:
        default_conf["is_debug"] = True
        default_conf["use_https"] = False
        default_conf["server_name"] = "localhost:5000"
        write_config(default_conf, config_template, config_path)
        sys.exit(0)

    # SERVER_NAME
    click.secho(
        "公开服务器的名称和端口号。如果 xfree 在端口 80 上可访问，您可以省略端口。例如，如果 xfree 可通过 example.org:8080 访问，那么这也是您在这里设置的内容。",
        fg="cyan",
    )
    default_conf["server_name"] = click.prompt(
        click.style("服务器名称", fg="magenta"),
        type=str,
        default=default_conf.get("server_name"),
    )

    # HTTPS 或 HTTP
    click.secho("xfree 是通过 HTTPS（推荐）还是 HTTP 提供服务？", fg="cyan")
    default_conf["use_https"] = click.confirm(
        click.style("使用 HTTPS？", fg="magenta"), default=default_conf.get("use_https")
    )

    # SQLALCHEMY_DATABASE_URI
    click.secho(
        "对于 Postgres 使用：\n    postgresql://xfree@localhost:5432/xfree\n有关更多选项，请参阅 SQLAlchemy 文档：\n    http://docs.sqlalchemy.org/en/latest/core/engines.html",
        fg="cyan",
    )
    default_conf["database_uri"] = click.prompt(
        click.style("数据库 URI", fg="magenta"),
        default=default_conf.get("database_uri"),
    )

    # REDIS_ENABLED
    click.secho(
        "Redis 将用于任务队列、缓存和速率限制等方面。",
        fg="cyan",
    )
    default_conf["redis_enabled"] = click.confirm(
        click.style("您想使用 Redis 吗？", fg="magenta"), default=True
    )  # default_conf.get("redis_enabled") is False

    # REDIS_URI
    if default_conf.get("redis_enabled", False):
        default_conf["redis_uri"] = click.prompt(
            click.style("Redis URI", fg="magenta"),
            default=default_conf.get("redis_uri"),
        )
    else:
        default_conf["redis_uri"] = ""

    # MAIL_SERVER
    click.secho(
        "要使用'localhost'，请确保您安装了 sendmail 或类似的东西。Gmail 也受支持。",
        fg="cyan",
    )
    default_conf["mail_server"] = click.prompt(
        click.style("邮件服务器", fg="magenta"),
        default=default_conf.get("mail_server"),
    )
    # MAIL_PORT
    click.secho("SMTP 服务器监听的端口。", fg="cyan")
    default_conf["mail_port"] = click.prompt(
        click.style("邮件服务器 SMTP 端口", fg="magenta"),
        default=default_conf.get("mail_port"),
    )
    # MAIL_USE_TLS
    click.secho(
        "如果您使用本地 SMTP 服务器（如 sendmail），则不需要此选项。对于外部服务器，它是必需的。",
        fg="cyan",
    )
    default_conf["mail_use_tls"] = click.confirm(
        click.style("使用 TLS 发送邮件？", fg="magenta"),
        default=default_conf.get("mail_use_tls"),
    )
    # MAIL_USE_SSL
    click.secho("与上述相同。TLS 是 SSL 的后继者。", fg="cyan")
    default_conf["mail_use_ssl"] = click.confirm(
        click.style("使用 SSL 发送邮件？", fg="magenta"),
        default=default_conf.get("mail_use_ssl"),
    )
    # MAIL_USERNAME
    click.secho(
        "如果您使用本地 SMTP 服务器，则不需要此选项。对于 Gmail，您必须在此处输入您的电子邮件地址。",
        fg="cyan",
    )
    default_conf["mail_username"] = click.prompt(
        click.style("邮件用户名", fg="magenta"),
        default=default_conf.get("mail_username"),
    )
    # MAIL_PASSWORD
    click.secho(
        "如果您使用本地 SMTP 服务器，则不需要此选项。对于 Gmail，您必须在此处输入您的 Gmail 密码。",
        fg="cyan",
    )
    default_conf["mail_password"] = click.prompt(
        click.style("邮件密码", fg="magenta"),
        default=default_conf.get("mail_password"),
    )
    # MAIL_DEFAULT_SENDER
    click.secho(
        "发件人的名称。您可能希望将其更改为类似于'<您的社区> Mailer'的内容。",
        fg="cyan",
    )
    default_conf["mail_sender_name"] = click.prompt(
        click.style("邮件发件人名称", fg="magenta"),
        default=default_conf.get("mail_sender_name"),
    )
    click.secho(
        "在本地主机上，您希望在此处使用一个 noreply 地址。对于 Gmail，请在此处使用您的电子邮件地址。",
        fg="cyan",
    )
    default_conf["mail_sender_address"] = click.prompt(
        click.style("邮件发件人地址", fg="magenta"),
        default=default_conf.get("mail_sender_address"),
    )
    # ADMINS
    click.secho(
        "日志和重要系统消息将发送到此地址。对于 Gmail，请在此处使用您的电子邮件地址。",
        fg="cyan",
    )
    default_conf["mail_admin_address"] = click.prompt(
        click.style("邮件管理员电子邮件", fg="magenta"),
        default=default_conf.get("mail_admin_address"),
    )

    click.secho(
        "可选的文件路径，用于从其中加载日志配置文件。有关更多详细信息，请参阅 Python 日志记录文档。\n    https://docs.python.org/library/logging.config.html#logging-config-fileformat",
        fg="cyan",
    )
    default_conf["log_config_path"] = click.prompt(
        click.style("日志配置路径", fg="magenta"),
        default=default_conf.get("log_config_path"),
    )

    deprecation_mesg = (
        "弃用警告级别。选项有：\n"
        "\terror\t将弃用警告转换为异常\n"
        "\tignore\t从不警告弃用\n"
        "\talways\t始终警告弃用，即使警告已发出\n"
        "\tdefault\t每次使用时显示一次弃用警告\n"
        "\tmodule\t每个模块显示一次弃用警告\n"
        "\tonce\t无论位置如何，仅显示一次弃用警告\n"
        "如果您不确定，请选择 default\n"
        "有关更多详细信息，请参阅：https://docs.python.org/3/library/warnings.html#the-warnings-filter"
    )

    click.secho(deprecation_mesg, fg="cyan")
    default_conf["deprecation_level"] = click.prompt(
        click.style("弃用警告级别", fg="magenta"),
        default=default_conf.get("deprecation_level"),
    )

    write_config(default_conf, config_template, config_path)

    # 完成
    click.secho(
        "配置文件已保存到：\n{cfg}\n您可以根据需要随意调整它。".format(cfg=config_path),
        fg="blue",
        bold=True,
    )
    click.secho(
        "用法：\nxfree --config {cfg} run".format(cfg=config_path), fg="green"
    )



